// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;

namespace SnapObjects.Data
{
    internal class PropertyAccessor<TModel, TValue> : IPropertyAccessor
    {
        private readonly TypeCode _valueTypeCode;
        private readonly bool _handleCompatible;
        private bool _isUnsignedType;

        private readonly Func<TModel, TValue> _getter;
        private readonly Action<TModel, TValue> _setter;
        private readonly PocoPropertyMeta _property;

        public PropertyAccessor(
            PocoPropertyMeta property,
            Func<TModel, TValue> getter,
            Action<TModel, TValue> setter)
        {
            _property = property;
            _getter = getter;
            _setter = setter;

            var type = typeof(TValue);

            _valueTypeCode = _property.IsNullable
                ? Type.GetTypeCode(Nullable.GetUnderlyingType(type))
                : Type.GetTypeCode(type);

            _handleCompatible = this.HandleCompatible(_valueTypeCode);
        }

        public object GetValue(object model)
        {
            if (_getter != null)
            {
                if (model == null)
                {
                    throw new PointMinusOneException();
                }

                return _getter((TModel)model);
            }
            else
            {
                throw new InvalidOperationException(
                    $"'{_property.Name}' No getter method support.");
            }
        }

        public void SetValue(object model, object value)
        {
            if (_setter != null)
            {
                try
                {
                    var comatibleValue = value is TValue tv
                        ? tv : this.GetCompatibleValue(value);

                    _setter((TModel)model, comatibleValue);
                }
                catch (NullReferenceException)
                {
                    throw new InvalidOperationException(String.Format(
                        Properties.Resource.PROPERTY_NULLVALUE_EXCEPTION, _property.Name));
                }
            }
            else
            {
                throw new InvalidOperationException(
                    $"'{_property.Name}' No setter method support.");
            }
        }

        public bool TryGetValue(object model, out object value)
        {
            if (_getter != null)
            {
                value = _getter((TModel)model);

                return true;
            }

            value = null;

            return false;
        }

        public bool TrySetValue(object model, object value)
        {
            if (_setter != null)
            {
                try
                {
                    this.SetValue(model, value);

                    return true;
                }
                catch (Exception)
                {
                    return false;
                }
            }

            return false;
        }

        public TValue GetCompatibleValue(object value)
        {
            if (value != null && _handleCompatible)
            {
                var tempType = value.GetType();

                var type = Nullable.GetUnderlyingType(tempType) ?? tempType;

                var typeCode = Type.GetTypeCode(type);

                if (typeCode > TypeCode.Char && typeCode < _valueTypeCode)
                {
                    if (_valueTypeCode == TypeCode.Decimal)
                    {
                        if (typeCode != TypeCode.Double && typeCode != TypeCode.Single)
                        {
                            value = Convert.ChangeType(value, _valueTypeCode);
                        }
                    }
                    else if (!_isUnsignedType || (_isUnsignedType && this.IsUnsignedType(typeCode)))
                    {
                        value = Convert.ChangeType(value, _valueTypeCode);
                    }
                }
            }

            return (TValue)value;
        }

        public bool HandleCompatible(TypeCode typeCode)
        {
            switch (typeCode)
            {
                case TypeCode.Double:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.SByte:
                case TypeCode.Single:
                case TypeCode.Decimal:
                    return true;
                case TypeCode.Byte:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                {
                    _isUnsignedType = true;
                    return true;
                }
            }

            return false;
        }

        private bool IsUnsignedType(TypeCode typeCode)
        {
            switch (typeCode)
            {
                case TypeCode.Byte:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                    return true;
            }

            return false;
        }
    }
}
